require "test/unit";

class Person
  attr_reader :name, :age, :friends;
 
  class WrongAgeError < Exception; end
  class DoubleFriendError < Exception; end
  class NoFriendError < Exception; end
  class FriendshipLevelExceeded < Exception; end
  
  MaximumFriendshipLevel = 5;
  
  def initialize(name, age)
    raise WrongAgeError, "A Person cannot have less than 0 or more than 200 of age" if age < 0 or age > 200;
    @name, @age = name, age;
    @friends = [];
  end
  
  def add_friends(*friends)
    friends.each do |friend|
      unless @friends.include?(friend)
        @friends << friend;
      else
        raise DoubleFriendError, "You've already had the friend #{friend.name}";        
      end
    end
  end
  
  def remove_friend(friend)
    if @friends.include?(friend)
      @friends.delete(friend);
    else
      raise NoFriendError, "You cannot remove an unexistent friend, #{friend.name} isn't your friend";
    end
  end
  
  def friend?(friend)
    @friends.include?(friend);
  end
  
  alias_method :<<, :add_friends;
  
  def friendship(target_friend)
    friend_stack = [];
    if friend?(target_friend)
      friend_stack = [self, target_friend];
    else
      @friends.each do |friend|
        friends = friend.friendship(target_friend);
        friend_stack = [self] + friends unless friends == [] || friends.size >= MaximumFriendshipLevel;
      end
    end
    friend_stack;
  end
 
end


class PersonTest < Test::Unit::TestCase
 
  def setup
  end
 
  def test_initialize
    msg = "Initializing with an negative age or more than 200 shouldn't work."
    assert_raises(Person::WrongAgeError, msg){Person.new "Somebody", -15}
    assert_raises(Person::WrongAgeError, msg){Person.new "Someone", 500}
    assert_nothing_raised{person = Person.new "Paul", 35}
  end
 
  def test_adding_friends
    people = create_people;
    assert_nothing_raised do
      # The method << should be equal to add_friends
      people[:paul] << people[:anne];
      people[:anne].add_friends(people[:robb], people[:paul]);
      people[:robb] << people[:paul];
    end
    assert people[:paul].friend?(people[:anne]);
    assert people[:anne].friend?(people[:robb]) && people[:anne].friend?(people[:robb]);
    assert !people[:paul].friend?(people[:robb]), "Even if some friend have you as a friend you don't have this person as your friend";
    assert_raises(Person::DoubleFriendError, "Adding an existing friend should result in error") do
      people[:paul] << people[:anne];
    end
  end
  
  def test_remove_friend
    people = create_people;
    people[:paul] << people[:anne];
    assert people[:paul].friend?(people[:anne]);
    assert_nothing_raised{people[:paul].remove_friend(people[:anne])}
    assert !people[:paul].friend?(people[:anne]), "When a friend is already removed, it shouldn't appears as a friend";
    assert_raises(Person::NoFriendError, "Trying to remove a frient which was already removed should result in error") do
      people[:paul].remove_friend(people[:anne])
    end
    assert_raises(Person::NoFriendError, "Removing an unexistent friend should result in error") do
      people[:paul].remove_friend(people[:robb]);
    end
  end
 
 def test_friendship_level
    people = create_people;
    people[:paul] << people[:anne];
    people[:paul] << people[:robb];
    people[:anne] << people[:bill];
    people[:anne] << people[:robb];
    people[:bill] << people[:nath];
    people[:nath] << people[:eva];
    people[:eva] << people[:jason];
    # Assuming that the constant Person::MaximumFriendshipLevel is 4
    assert_equal [:paul, :anne, :bill, :nath, :eva].map{|s| people[s]}, people[:paul].friendship(people[:eva]);
    assert_equal [:anne, :robb].map{|s| people[s]}, people[:anne].friendship(people[:robb]);
    assert_equal [], people[:bill].friendship(people[:paul]), "When we don't have a friendship with anyone it should return an empty array";
    assert_equal [], people[:paul].friendship(people[:jason]), "When Person::MaximumFriendshipLevel is reachup in the recursivily call, it should return an empty array";
  end  
  
  private
  def create_people
    return Hash[*{
      :paul => ["Paul", 35], 
      :anne => ["Anne", 25], 
      :eva => ["Eva", 28], 
      :robb => ["Robb", 18],
      :bill => ["Bill", 20], 
      :nath => ["Nath", 34],
      :jason => ["Jason", 21],
      :matz => ["Yukihiro", 25],
      :david => ["David", 25]
    }.map{|k, args| [k, Person.new(*args)]}.flatten];
  end
  
end